package se.slackers.sounder.algorithm;

import java.applet.Applet;
import java.awt.Color;
import java.awt.Event;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.util.Arrays;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.SourceDataLine;

@SuppressWarnings("serial")
public class X extends Applet implements Runnable {
	private final static float RATE = 1 * 11050.0f;// Music sample rate
	private final static float PISTEP = 6.28318531f / RATE;
	private final static int BUFFER_SIZE = (int) (RATE * 2);
	private final static int BEAT = (BUFFER_SIZE >> 4);

	private static final int DRUMS = 0;
	private static final int BASE = 1;
	private static final int LEAD = 2;

	private static final float DRUM_VOLUME = 80;
	private static final float BASE_VOLUME = 50;
	private static final float LEAD_VOLUME = 60;

	private SourceDataLine dataline = null;

	@Override
	public void start() {
		// start the audio thread
		new Thread(this).start();
	}

	public void run() {
		if (dataline == null) {
			/*********/
			/* AUDIO */
			/*********/

			try {
				final AudioFormat format = new AudioFormat(RATE, 8, 1, true, false);
				final DataLine.Info dli = new DataLine.Info(SourceDataLine.class, format, BUFFER_SIZE);
				dataline = (SourceDataLine) AudioSystem.getLine(dli);
				dataline.open();
				dataline.start();
			} catch (final Exception e) {
				e.printStackTrace(); // display error, but keep going
			}

			final byte[] buffer = new byte[BUFFER_SIZE];

			// start the graphics thread
			new Thread(this).start();

			// generate the frequencies of some notes
			final float[] freq = new float[32];
			// float f = 110.0f; // A
			float f = 65.41f; // C
			for (int i = 1; i < freq.length; i++) {
				freq[i] = f;
				f *= 1.0594630944f; // Evenly tempered scale (12th root 2)
			}

			final int[][][] patterns = new int[3][][];
			final int[][] patternOrder = new int[3][];
			final int[] patternIndex = new int[] { 0, 0, 0 };

			// DRUMS
			patternOrder[DRUMS] = new int[] { 0, 1, 0, 2 };
			patterns[DRUMS] = new int[][] { new int[] { 2, 0, 2, 0, 1, 0, 2, 0, 2, 0, 2, 0, 1, 0, 2, 0 }, // Beat
																											// 0
					new int[] { 2, 0, 2, 0, 1, 0, 2, 0, 2, 0, 2, 0, 1, 0, 2, 2 }, // Beat 1
					new int[] { 2, 0, 2, 0, 1, 0, 2, 0, 2, 0, 2, 0, 1, 0, 1, 0 }
			// Beat 2
			};

			// A A# B C C# D D# E F F# Template_Raw Template_Raw# A2 A2# B2 B2# C2 C2# D2
			// 1 _2 3 4 _5 6 _7 8 9 _a b _c _d _e_ _f _10 11 _12 13

			// BASE:ABB-AAB-|AABBABD2B|D2F#F#-EF#F#-|E-F#F#EF#AF#
			patternOrder[BASE] = new int[] { 0, 1, 2, 3 };
			patterns[BASE] = new int[][] { new int[] { 10, 10, 12, 12, 12, 12, 0, 0, 10, 10, 10, 10, 12, 12, 0, 0 }, // Beat
																														// 0
					new int[] { 10, 10, 10, 10, 12, 12, 12, 12, 10, 10, 12, 12, 15, 15, 12, 12 }, // Beat
																									// 1
					new int[] { 15, 15, 7, 7, 7, 7, 0, 0, 5, 5, 7, 7, 7, 7, 0, 0 }, // Beat 2
					new int[] { 5, 5, 0, 0, 7, 7, 7, 7, 5, 5, 7, 7, 10, 10, 7, 7 } // Beat 3
			};

			// LEAD 1: --C2#BBAAB|BBE2C2#C2#AAF#|F#-A-B-C2#-|E2C2#-B-E2C2#B
			// LEAD 2: --F#ABC2#-E2|-C2#C2#ABBF#F#|EEEF#F#AAB|BC2#E2E2BBB-
			patternOrder[LEAD] = new int[] { 9, 9, 9, 9, 4, 5, 6, 7, 0, 1, 2, 3, 8, 9, 9, 9 };
			patterns[LEAD] = new int[][] { new int[] { 0, 0, 0, 0, 0, 0, 14, 14, 12, 12, 12, 12, 10, 10, 10, 10 }, // Beat
																													// 0
					new int[] { 12, 12, 12, 12, 12, 12, 17, 17, 14, 14, 14, 14, 10, 10, 10, 10 }, // Beat
																									// 1
					new int[] { 7, 7, 7, 7, 0, 0, 10, 10, 0, 0, 12, 12, 0, 0, 14, 14 }, // Beat 2
					new int[] { 0, 0, 17, 17, 14, 14, 0, 0, 12, 12, 0, 0, 17, 17, 14, 14 }, // Beat
																							// 3

					new int[] { 0, 0, 0, 0, 7, 7, 10, 10, 12, 12, 14, 14, 0, 0, 17, 17 }, // Beat 4
																							// (VAR
																							// 2)
					new int[] { 0, 0, 14, 14, 14, 14, 10, 10, 12, 12, 12, 12, 7, 7, 7, 7 }, // Beat
																							// 5
																							// (VAR
																							// 2)
					new int[] { 5, 5, 5, 5, 5, 5, 7, 7, 7, 7, 10, 10, 10, 10, 12, 12 }, // Beat 6
																						// (VAR 2)
					new int[] { 12, 12, 14, 14, 17, 17, 17, 17, 12, 12, 12, 12, 12, 12, 0, 0 }, // Beat
																								// 7
																								// (VAR
																								// 2)

					new int[] { 12, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // Beat 8
																					// (Special END)
					new int[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } // Beat 9 (EMPTY)
			};
			// generate sound
			while (true) {
				int offset = 0;
				for (int beat = 0; beat < 16; beat++) {
					// clear the buffer first
					Arrays.fill(buffer, offset, offset + BEAT, (byte) 0);

					// render the notes
					for (int instrument = 0; instrument < patterns.length; instrument++) {
						// find out which pattern to play
						final int index = patternIndex[instrument];
						final int order = patternOrder[instrument][index];
						final int[] pattern = patterns[instrument][order];
						final int note = pattern[beat];

						if (note <= 0) {
							// nothing to play here
							continue;
						}

						// render the samples
						final int noteLimit = instrument == 0 ? BEAT / note : BEAT;
						for (int i = 0; i < noteLimit; i++) {
							byte ch = 0;
							switch (instrument) {
							case DRUMS:
								ch = (byte) (Math.random() * DRUM_VOLUME);
								break;
							case BASE:
								ch = (byte) (Math.sin(freq[note] * (offset + i) * PISTEP) * BASE_VOLUME);
								break;
							case LEAD:
								ch = (byte) (Math.sin(freq[note] * 4 * (offset + i) * PISTEP) * LEAD_VOLUME);
								break;
							}

							// put the sample into the audio buffer
							int value = buffer[offset + i] + ch;
							if (value > 127) {
								value = 127;
							}
							if (value < -128) {
								value = -128;
							}
							buffer[offset + i] = (byte) (value);
						}
					}

					offset += BEAT;
				}

				// Increment the pattern index counter
				for (int i = 0; i < patterns.length; i++) {
					patternIndex[i] = (patternIndex[i] + 1) % patternOrder[i].length;
				}

				dataline.write(buffer, 0, buffer.length);

				// try {
				// final FileOutputStream out = new FileOutputStream(new File("out.raw"));
				// out.write(buffer);
				// out.close();
				// } catch (final FileNotFoundException e) {
				// throw new RuntimeException(e);
				// } catch (final IOException e) {
				// throw new RuntimeException(e);
				// }
			}
			// unreachable point
		}

		/************/
		/* GRAPHICS */
		/************/

		setSize(800, 600); // For AppletViewer, remove later.

		// Set up the graphics stuff, double-buffering.
		final BufferedImage screen = new BufferedImage(800, 600, BufferedImage.TYPE_INT_RGB);
		final Graphics g = screen.getGraphics();
		final Graphics appletGraphics = getGraphics();

		// Some variables to use for the fps.
		int tick = 0, fps = 0, acc = 0;
		long lastTime = System.nanoTime();

		// Game loop.
		while (true) {
			final long now = System.nanoTime();
			acc += now - lastTime;
			tick++;
			if (acc >= 1000000000L) {
				acc -= 1000000000L;
				fps = tick;
				tick = 0;
			}
			lastTime = now;

			// GAME LOGIC

			// RENDER
			g.setColor(Color.black);
			g.fillRect(0, 0, 800, 600);
			g.setColor(Color.white);
			g.drawString("FPS " + String.valueOf(fps), 20, 30);

			// Draw the entire results on the screen.
			appletGraphics.drawImage(screen, 0, 0, null);

			try {
				Thread.sleep(10);
			} catch (final Exception e) { /* best practice */
			}

			if (!isActive()) {
				return;
			}
		}
	}

	@Override
	public boolean handleEvent(final Event e) {
		switch (e.id) {
		case Event.KEY_PRESS:
		case Event.KEY_ACTION:
			// key pressed
			break;
		case Event.KEY_RELEASE:
			// key released
			break;
		case Event.MOUSE_DOWN:
			// mouse button pressed
			break;
		case Event.MOUSE_UP:
			// mouse button released
			break;
		case Event.MOUSE_MOVE:
			break;
		case Event.MOUSE_DRAG:
			break;
		}
		return false;
	}
}